systemd service pattern
Each API runs as its own systemd unit. Units live in /etc/systemd/system/<api-name>.service.
Standard unit file
See templates/api.service.template for the canonical copy. Key design choices:
| Setting | Value | Why |
|---|---|---|
User= / Group= | apisvc | Unprivileged dedicated account |
WorkingDirectory= | /opt/apis/<api-name> | Relative imports and app paths work |
EnvironmentFile= | /etc/<api-name>/<api-name>.env | Secrets outside git |
--host 127.0.0.1 | localhost only | Nginx is the only public entry |
Restart=always | auto-restart | Survives crashes and reboots |
RestartSec=5 | delay before restart | Avoid tight crash loops |
FastAPI + Uvicorn (default)
ExecStart=/opt/venvs/<api-name>/bin/uvicorn app.main:app \
--host 127.0.0.1 \
--port 8001
Adjust app.main:app to match the project's ASGI application object.
Flask + Gunicorn (alternative)
ExecStart=/opt/venvs/<api-name>/bin/gunicorn \
--bind 127.0.0.1:8001 \
--workers 2 \
app:app
Use Gunicorn when the project is WSGI (Flask, Django without ASGI). Worker count: commonly 2 * CPU + 1 for CPU-bound work; start with 2 on a laptop server.
Environment file format
/etc/<api-name>/<api-name>.env uses systemd EnvironmentFile syntax:
- One
KEY=valueper line - No
exportprefix - Quotes optional; use quotes if values contain spaces
- Lines starting with
#are comments
Example:
ENV=production
LOG_LEVEL=info
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;...
After editing:
sudo systemctl restart <api-name>
systemd reads the file at start time; changes require a restart (not reload) unless using systemctl daemon-reload for unit file changes only.
Logging
stdout/stderr go to the journal by default:
# Follow live logs
journalctl -u <api-name> -f
# Last 100 lines
journalctl -u <api-name> -n 100 --no-pager
# Since last boot
journalctl -u <api-name> -b
Optional file logging: add StandardOutput=append:/var/log/<api-name>/app.log and ensure /var/log/<api-name>/ exists with apisvc ownership. Journal logging is usually sufficient.
Common operations
# Apply unit file changes
sudo systemctl daemon-reload
sudo systemctl restart <api-name>
# Enable on boot
sudo systemctl enable <api-name>
# Status
sudo systemctl status <api-name>
# Stop temporarily
sudo systemctl stop <api-name>
Security hardening (optional)
For tighter isolation, add to the [Service] section:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/<api-name>
Test thoroughly after enabling ProtectSystem=strict — some apps need write access to unexpected paths.
Dependency ordering
If an API must start after network is up (default):
[Unit]
After=network-online.target
Wants=network-online.target
If multiple APIs depend on each other (uncommon), use After=<other-api>.service — prefer loose coupling via HTTP instead.